node/cn: verify a blob tx sidecar only once - #1012
Merged
ian0371 merged 3 commits intoAug 12, 2026
Merged
Conversation
A replayed blob transaction is decoded into a fresh sidecar object, so the sidecar's own validation cache never hits and the handler verified its KZG proofs again on every copy. One single-blob v1 sidecar measures ~15.5ms, and a 12MiB TxMsg fits about 90 of them, so a captured transaction could be resent indefinitely at no cost to the sender. The handler now keeps a bounded set of blob tx hashes it already verified and skips the verification for those. Constraint: the tx pool verifies the sidecar independently before admitting a transaction, so skipping the early check cannot let a bad sidecar through Rejected: reject duplicate hashes within one message | subsumed, the set already skips the repeats inside a single batch Rejected: global KZG semaphore or per-peer token buckets | the repeated verification is what made a small message expensive, and bounding concurrency would also delay legitimate propagation Confidence: high Scope-risk: narrow Not-tested: a rotating set of distinct captured blob txs still costs one verification each, which is bandwidth-bound Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ian0371
reviewed
Aug 10, 2026
|
|
||
| // The tx hash does not cover the sidecar, so the replay carries a broken proof | ||
| // under the same hash and passes only because the verification is skipped. | ||
| blobTx.BlobTxSidecar().Proofs[0][0] ^= 0xFF |
Collaborator
There was a problem hiding this comment.
The cache key doesn't cover the sidecar, so this happens. Is this ok?
Contributor
Author
There was a problem hiding this comment.
Changed to keccak(blobHashes ‖ Version ‖ Blobs ‖ Proofs)
2dvorak
reviewed
Aug 11, 2026
2dvorak
left a comment
Contributor
There was a problem hiding this comment.
tx.Hash()as the key is bypassable at zero cost. The expensive work is per-sidecar, but the key is per-transaction, and the attacker controls that mapping: one precomputed sidecar attaches to unlimited distinct transactions by bumping a nonce — identical sidecar bytes, different tx hashes — so every one of them misses the set and pays a full verification.- Reject blob txs with zero blob hashes.
ValidateWithBlobHashespasses vacuously when there are no blobs so a 113-byte blob tx with an empty sidecar validates. The pool rejects these unconditionally (tx_pool.go:1071), so the handler accepts and forwards a transaction that can never be valid and never disconnects the peer for it. It also mints a cache entry per tx, cheap enough to flush the whole set for ~113 KiB — though that part goes away once the key changes per (1).
The set was keyed by transaction hash, which does not cover the sidecar. A sender could bump the nonce to replay one sidecar under unlimited distinct hashes, so every copy missed the set and paid a full verification, and a sidecar swapped under an already verified hash hit the set and skipped verification entirely. Key the set by what the verification consumes instead: the blob hashes, the sidecar version, its blobs and its proofs. Commitments are excluded because each blob hash is the sha256 of one, so the hashes already pin them. Also reject a blob transaction that declares no blob hashes. Every length check in the verification compares against the declared hashes, so it passes vacuously, while the pool rejects such a transaction unconditionally - the handler forwarded something that could never be valid and left the sender connected. Constraint: the key has to cover the blobs, not only the proofs, since the proofs are verified against them Rejected: keep the transaction hash and add a sidecar fingerprint | any key that omits the blobs is bypassable the same way Confidence: high Scope-risk: narrow Not-tested: a rotating set of distinct captured blob txs still costs one verification each, which is bandwidth-bound Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Author
|
@2dvorak The key now covers what the verification consumes, and rejects blobless tx earlier before the veficiation runs. |
2dvorak
reviewed
Aug 11, 2026
The key left out the commitments, on the grounds that each blob hash is the sha256 of one. That binding is enforced by ValidateBlobCommitmentHashes, which is exactly what a cache hit skips, so a commitment could be swapped under an already verified sidecar: the key still matched, the verification never ran, and the peer stayed connected where it is otherwise dropped. The elements are also fixed-size and were concatenated without their counts, so sidecars of different shapes encode to the same bytes and share a key the same way. Add the commitments and prefix the four element counts. The key is then an injective encoding of everything ValidateWithBlobHashes reads, so a cache hit means that exact input passed before and the handler rejects what it rejected before this cache existed. Constraint: anything the verification reads has to be in the key, since the key decides whether the verification runs at all Rejected: keep the commitments out and rely on the hashes | the check that ties them together is the one being skipped Rejected: encode with RLP instead of prefixing counts | copies the whole sidecar into a buffer before hashing, on the path this cache exists to keep cheap Confidence: high Scope-risk: narrow Not-tested: the count prefix has no test of its own Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2dvorak
approved these changes
Aug 12, 2026
ian0371
approved these changes
Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposed changes
A replayed blob transaction is decoded into a fresh sidecar object, so the sidecar's own validation cache never hits and the handler verified its KZG proofs again on every copy. The handler now skips verification for blob tx hashes it already verified; the tx pool still verifies the sidecar independently before admitting the transaction.
Types of changes
Checklist
I have read the CLA Document and I hereby sign the CLAin first time contribute after having read CLA$ make test)Related issues
Further comments